﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Configuration;
using System.Xml;
using System.Xml.Serialization;
using System.IO;
using System.Web.Hosting;
using System.Threading;

namespace UserManagementWCFService
{
    class UserDataManager
    {
        private static UserDataManager instance = new UserDataManager();

        private UserCollection users = new UserCollection();

        private HashSet<String> usernames = new HashSet<string>();

        private SortedDictionary<Guid, String> userIdToUsername = new SortedDictionary<Guid, string>();

        private SortedDictionary<String, User> usernameToUser = new SortedDictionary<string, User>();

        private String xmlFilename = ConfigurationManager.AppSettings["UserXmlFile.xml"];

        private XmlSerializer userListSerializer = new XmlSerializer(typeof(UserCollection));

        private ReaderWriterLock readerWriterLock = new ReaderWriterLock();

        public static UserDataManager INSTANCE
        {
            get { return instance; }
        }

        private UserDataManager()
        {
            //Read the xml into our user list;
            readUsers();
            //Add a delgate to write our user list to the xml file when the
            //application shuts down.
            AppDomain.CurrentDomain.ProcessExit += (s, e) => writeUsers();
        }

        private void readUsers()
        {
            // Create a TextReader to read the file. 
            FileStream fs = new FileStream(HostingEnvironment.MapPath(xmlFilename), FileMode.OpenOrCreate);
            TextReader reader = new StreamReader(fs);
            //Do some preliminary checks to see if the file is well formed and has information
            XmlDocument document;
            if ( (document = validFile(reader)) != null)
            {
                //Get the root of the document.
                XmlNode child = document.SelectSingleNode("UserCollection");
                if (child != null)
                {
                    XmlNodeReader nr = new XmlNodeReader(child);
                    //Create a deserializer to read in the data in the file.
                    users = (UserCollection)userListSerializer.Deserialize(nr);
                    init();
                }
                else
                {
                    throw new InvalidDataException("Cannot use Xml File." + 
                        "Does not have root element \"UserCollection\"");
                }
            }
            fs.Close();
        }

        private XmlDocument validFile(TextReader reader)
        {
            try
            {
                XmlDocument xmlDoc = new XmlDocument();
                xmlDoc.Load(reader);
                return xmlDoc;
            }
            catch (System.Xml.XmlException)
            {
                return null;
            }
        }

        private void init()
        {
            foreach (User user in users.Users)
            {
                addUserToMaps(user);
            }
        }

        private void addUserToMaps(User user)
        {
            //Add the user to the necessary maps and lists to track the user. 
            usernames.Add(user.Username);
            userIdToUsername.Add(user.Identifier, user.Username);
            usernameToUser.Add(user.Username, user);
        }

        /// <summary>
        /// Write each of the users that are currently being tracked into the data
        /// structure.
        /// </summary>
        private void writeUsers()
        {
            // Create a TextReader to read the file. 
            readerWriterLock.AcquireWriterLock(3000);
            try
            {
                FileStream fs = new FileStream(HostingEnvironment.MapPath(xmlFilename), FileMode.OpenOrCreate);
                TextWriter writer = new StreamWriter(fs);
                userListSerializer.Serialize(writer, users);
                fs.Close();
            }
            finally
            {
                readerWriterLock.ReleaseWriterLock();
            }
        }

        /// <summary>
        /// Adds the specified user to the data manager. 
        /// </summary>
        /// <param name="user">The user that needs to be added to
        /// the data manager.</param>
        /// <returns>The user that was added to the manager.</returns>
        public User addUser(User user)
        {
            //if our user doesn't have a guid, then add one.
            if (user.Identifier == null)
            {
                user.Identifier = Guid.NewGuid();
            }
            readerWriterLock.AcquireWriterLock(3000);
            try
            {
                users.Add(user);
                addUserToMaps(user);
                Thread writerThread = new Thread(new ThreadStart(writeUsers));
                writerThread.Start();
            }
            finally
            {
                //Release the writer lock.
                readerWriterLock.ReleaseWriterLock();
            }
            return user;
        }

        /// <summary>
        /// Get the user with the given id. This method synchronizes using a reader lock
        /// in order to ensure we are getting the most up to date information.
        /// </summary>
        /// <param name="userId">The user ID that we are looking for.</param>
        /// <returns>The User with the corresponding ID. If we don't have it, then
        /// return null.</returns>
        public User getUser(Guid userId)
        {
            readerWriterLock.AcquireReaderLock(3000);
            var username = userIdToUsername[userId];
            var user = usernameToUser[username];
            //Release the lock.
            readerWriterLock.ReleaseReaderLock();
            return user;
        }

        /// <summary>
        /// Check to see if a user with the given ID is being managed by the data
        /// manager. This method is synchronized using a lock. 
        /// </summary>
        /// <param name="user">The ID of the user of interest.</param>
        /// <returns>True if the Guid for the user is currently being managed and
        /// false otherwise.</returns>
        public bool containsUser(Guid userId)
        {
            bool containsUser = false;
            lock (userIdToUsername)
            {
                containsUser = userIdToUsername.ContainsKey(userId);
            }
            return containsUser;
        }

        public bool containsUser(String username)
        {
            bool containsUser = false;
            lock (usernameToUser)
            {
                containsUser = usernameToUser.ContainsKey(username);
            }
            return containsUser;
        }

        /// <summary>
        /// Gets the id of the user with the specified name. Throws a UserNotFoundException
        /// if we don't have the user.
        /// </summary>
        /// <param name="username"></param>
        /// <returns></returns>
        public Guid getId(String username)
        {
            Guid id = Guid.NewGuid();
            bool foundId = false;
            lock (usernameToUser)
            {
                if (usernameToUser.ContainsKey(username))
                {
                    id = usernameToUser[username].Identifier;
                    foundId = true;
                }
            }
            if (!foundId)
            {
                throw new NoUserException(
                    string.Format("No user stored with username: ${0}", new string[]{username}));
            }
            return id;
        }


        internal User getUser(string username)
        {
            readerWriterLock.AcquireReaderLock(3000);
            User user = null;
            if(usernameToUser.ContainsKey(username))
            {
                user = usernameToUser[username];
            }
            //Release the lock.
            readerWriterLock.ReleaseReaderLock();
            return user;
        }


    }

    public class UserCollection
    {
        List<User> users = new List<User>();

        public void Add(User user)
        {
            users.Add(user);
        }

        public void Remove(User user)
        {
            users.Remove(user);
        }

        public List<User> Users
        {
            get { return users; }
            set { users = value; }
        }

    }
}
